home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Linux / Kubuntu 8.10 / kubuntu-8.10-desktop-i386.iso / casper / filesystem.squashfs / usr / bin / amarok_proxy.rb < prev    next >
Text File  |  2008-08-13  |  7KB  |  239 lines

  1. #!/usr/bin/env ruby
  2. #
  3. # Proxy server for Last.fm and DAAP. Relays the stream from the server to localhost, and
  4. # converts the protocol to http on the fly.
  5. #
  6. # (c) 2006 Paul Cifarelli <paul@cifarelli.net>
  7. # (c) 2006 Mark Kretschmann <markey@web.de>
  8. # (c) 2006 Michael Fellinger <manveru@weez-int.com>
  9. # (c) 2006 Ian Monroe <ian@monroe.nu>
  10. # (c) 2006 Martin Ellis <martin.ellis@kdemail.net>
  11. # (c) 2006 Alexandre Oliveira <aleprj@gmail.net>
  12. # (c) 2006 Tom Kaitchuck <tkaitchuck@comcast.net>
  13. #
  14. # License: GNU General Public License V2
  15.  
  16. # Amarok listens to stderr and recognizes these magic strings, do not remove them:
  17. # "AMAROK_PROXY: startup", "AMAROK_PROXY: SYNC"
  18.  
  19.  
  20. require 'socket'
  21. require "uri"
  22. $stdout.sync = true
  23.  
  24. class Proxy
  25.   ENDL = "\r\n"
  26.  
  27.   def initialize( port, remote_url, engine, proxy )
  28.     @engine = engine
  29.  
  30.     myputs( "running with port: #{port} and url: #{remote_url} and engine: #{engine}" )
  31.  
  32.     # Open the amarok-facing socket
  33.     # amarok: the server port on the localhost to which the engine will connect.
  34.     amarok = TCPServer.new( port )
  35.     myputs( "startup" )
  36.  
  37.     # amaroks: server socket for above.
  38.     amaroks = amarok.accept
  39.  
  40.     # uri: from amarok, identifies the source of the music
  41.     uri = URI.parse( remote_url )
  42.     myputs("host " << uri.host << " ")
  43.     myputs( port )
  44.  
  45.     # Now we have the source of the music, determine the HTTP request that
  46.     # needs to be made to the remote server (or remote proxy).  It will
  47.     # be of the form "GET ... HTTP/1.x".  It will include the
  48.     # http://hostname/ part if, and only if, we're using a remote proxy.
  49.     get = get_request( uri, !proxy.nil? )
  50.  
  51.     #Check for proxy
  52.     begin
  53.       proxy_uri = URI.parse( proxy )
  54.       serv = TCPSocket.new( proxy_uri.host, proxy_uri.port )
  55.     rescue
  56.       serv = TCPSocket.new( uri.host, uri.port )
  57.     end
  58.  
  59.     serv.sync = true
  60.     myputs( "running with port: #{uri.port} and host: #{uri.host}" )
  61.  
  62.     # Read the GET request from the engine
  63.     amaroks_get = amaroks.readline
  64.     myputs( amaroks_get.inspect )
  65.  
  66.     myputs( get.inspect )
  67.     myputs( "#{amaroks_get} but sending #{get}" )
  68.     serv.puts( get )
  69.  
  70.     # Copy the HTTP REQUEST headers from the amarok engine to the
  71.     # remote server, and signal end of headers.
  72.     myputs( "COPY from amarok -> serv" )
  73.     cp_to_empty_outward( amaroks, serv )
  74.     safe_write( serv, "\r\n\r\n" )
  75.  
  76.     # Copy the HTTP RESPONSE headers from the server back to the
  77.     # amarok engine.
  78.     myputs( "COPY from serv -> amarok" )
  79.     cp_to_empty_inward( serv, amaroks )
  80.  
  81.     if @engine == 'gst10-engine'
  82.       3.times do
  83.         myputs( "gst10-engine waiting for reconnect" )
  84.         sleep 1
  85.         break if amaroks.eof
  86.       end
  87.       amaroks = amarok.accept
  88.       safe_write( amaroks, "HTTP/1.0 200 OK\r\n\r\n" )
  89.       amaroks.each_line do |data|
  90.         myputs( data )
  91.         data.chomp!
  92.         break if data.empty?
  93.       end
  94.     end
  95.  
  96.     # Now stream the music!
  97.     myputs( "Before cp_all()" )
  98.     cp_all_inward( serv, amaroks )
  99.  
  100.     if @engine == 'helix-engine' && amaroks.eof
  101.       myputs( "EOF Detected, reconnecting" )
  102.       amaroks = amarok.accept
  103.       cp_all_inward( serv, amaroks )
  104.     end
  105.   end
  106.  
  107.   def safe_write( output, data )
  108.     begin
  109.         output.write data
  110.     rescue
  111.       myputs( "error from output.write, #{$!}" )
  112.       myputs( $!.backtrace.inspect )
  113.       break
  114.     end
  115.   end
  116.  
  117.   def cp_to_empty_outward( income, output )
  118.     myputs "cp_to_empty_outward( income => #{income.inspect}, output => #{output.inspect}"
  119.     income.each_line do |data|
  120.       if data =~ /User-Agent: xine\/([0-9.]+)/
  121.         version = $1.split(".").collect { |v| v.to_i }
  122.         myputs("Found xine user agent version #{version.join(".")}")
  123.         @xineworkaround = ( version[0] <= 1 && version[1] <= 1 && version[2] <= 2 )
  124.       end
  125.       myputs( data )
  126.       data.chomp!
  127.       safe_write( output, data )
  128.       myputs( "data sent.")
  129.       return if data.empty?
  130.     end
  131.   end
  132.  
  133.   def desync (data)
  134.       if data.gsub!( "SYNC", "" )
  135.         myputs( "SYNC" )
  136.       end
  137.   end
  138.  
  139.   def cp_to_empty_inward( income, output )
  140.     myputs( "cp_to_empty_inward( income => #{income.inspect}, output => #{output.inspect}" )
  141.     income.each_line do |data|
  142.       myputs( data )
  143.       safe_write( output, data )
  144.       return if data.chomp == ""
  145.     end
  146.   end
  147.  
  148.   def cp_all_inward( income, output )
  149.     myputs( "cp_all( income => #{income.inspect}, output => #{output.inspect}" )
  150.     if self.is_a?( LastFM ) and @xineworkaround
  151.       myputs( "Using buffer fill workaround." )
  152.       filler = Array.new( 4096, 0 )
  153.       safe_write( output, filler ) # HACK: Fill xine's buffer so that xine_open() won't block
  154.     end
  155.     if @engine == 'helix-engine'
  156.       data = income.read( 1024 )
  157.     else
  158.       data = income.read( 4 )
  159.     end
  160.     desync( data )
  161.     holdover = ""
  162.     loop do
  163.       begin
  164.         safe_write( output, data )
  165.       rescue
  166.         myputs( "error from o.write, #{$!}" )
  167.         break
  168.       end
  169.       newdata = income.read( 1024 )
  170.  
  171.       data = holdover + newdata[0..-5]
  172.       holdover = newdata[-4..-1]
  173.       desync( data )
  174.  
  175.       break if newdata == nil
  176.     end
  177.   end
  178. end
  179.  
  180. class LastFM < Proxy
  181. # Last.fm protocol:
  182. # Stream consists of pure MP3 files concatenated, with the string "SYNC" in between, which
  183. # marks a track change. The proxy notifies Amarok on track change.
  184.  
  185.   def get_request( remote_uri, via_proxy )
  186.     # remote_uri - the URI of the stream we want
  187.     # via_proxy - true iff we're going through another proxy
  188.     if via_proxy then
  189.       url = remote_uri.to_s
  190.     else
  191.       url = "#{remote_uri.path || '/'}?#{remote_uri.query}"
  192.     end
  193.     get = "GET #{url} HTTP/1.0" + ENDL
  194.     get += "Host: #{remote_uri.host}:#{remote_uri.port}" + ENDL + ENDL
  195.   end
  196.  
  197. end
  198.  
  199. class DaapProxy < Proxy
  200.   def initialize( port, remote_url, engine, hash, request_id, proxy )
  201.     @hash = hash
  202.     @requestId = request_id
  203.     super( port, remote_url, engine, proxy )
  204.   end
  205.  
  206.   def get_request( remote_uri, via_proxy )
  207.     # via_proxy ignored for now
  208.     get = "GET #{remote_uri.path || '/'}?#{remote_uri.query} HTTP/1.0" + ENDL
  209.     get += "Accept: */*" + ENDL
  210.     get += "User-Agent: iTunes/4.6 (Windows; N)" + ENDL
  211.     get += "Client-DAAP-Version: 3.0" + ENDL
  212.     get += "Client-DAAP-Validation: #{@hash}" + ENDL
  213.     get += "Client-DAAP-Access-Index: 2" + ENDL
  214.     get += "Client-DAAP-Request-ID: #{@requestId}" + ENDL
  215.     get += "Host: #{remote_uri.host}:#{remote_uri.port}" + ENDL + ENDL
  216.     get
  217.   end
  218. end
  219.  
  220. def myputs( string )
  221.    $stdout.puts( "AMAROK_PROXY: #{string}" )
  222. end
  223.  
  224. begin
  225.   myputs( ARGV )
  226.   if( ARGV[0] == "--lastfm" ) then
  227.     option, port, remote_url, engine, proxy = ARGV
  228.     LastFM.new( port, remote_url, engine, proxy )
  229.   else
  230.     option, port, remote_url, engine, hash, request_id, proxy = ARGV
  231.     DaapProxy.new( port, remote_url, engine, hash, request_id, proxy )
  232.   end
  233. rescue
  234.   myputs( $!.to_s )
  235.   myputs( $!.backtrace.inspect )
  236. end
  237.  
  238. puts( "exiting" )
  239.